/*******************************************************************************
 * Copyright (c) 2012 Pivotal Software, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Pivotal Software, Inc. - initial API and implementation
 *******************************************************************************/
package org.springsource.ide.eclipse.commons.frameworks.ui.internal.contentassist;

import java.util.ArrayList;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.fieldassist.IContentProposal;
import org.eclipse.jface.fieldassist.IContentProposalListener;
import org.eclipse.jface.fieldassist.IContentProposalProvider;
import org.eclipse.jface.fieldassist.IControlContentAdapter;
import org.eclipse.jface.fieldassist.IControlContentAdapter2;
import org.eclipse.jface.fieldassist.SimpleContentProposalProvider;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;

/**
 * ContentProposalAdapter can be used to attach content proposal behavior to a
 * control. This behavior includes obtaining proposals, opening a popup dialog,
 * managing the content of the control relative to the selections in the popup,
 * and optionally opening up a secondary popup to further describe proposals.
 * <p>
 * A number of configurable options are provided to determine how the control
 * content is altered when a proposal is chosen, how the content proposal popup
 * is activated, and whether any filtering should be done on the proposals as
 * the user types characters.
 * <p>
 * This class provides some overridable methods to allow clients to manually
 * control the popup. However, most of the implementation remains private.
 * @author Kris De Volder
 * @author Nieraj Singh
 * @since 3.2
 */
public class ContentProposalAdapter {

	/*
	 * The lightweight popup used to show content proposals for a text field. If
	 * additional information exists for a proposal, then selecting that
	 * proposal will result in the information being displayed in a secondary
	 * popup.
	 */
	class ContentProposalPopup extends PopupDialog {
		/*
		 * The listener we install on the popup and related controls to
		 * determine when to close the popup. Some events (move, resize, close,
		 * deactivate) trigger closure as soon as they are received, simply
		 * because one of the registered listeners received them. Other events
		 * depend on additional circumstances.
		 */
		private final class PopupCloserListener implements Listener {
			private boolean scrollbarClicked = false;

			public void handleEvent(final Event e) {

				// If focus is leaving an important widget or the field's
				// shell is deactivating
				if (e.type == SWT.FocusOut) {
					scrollbarClicked = false;
					/*
					 * Ignore this event if it's only happening because focus is
					 * moving between the popup shells, their controls, or a
					 * scrollbar. Do this in an async since the focus is not
					 * actually switched when this event is received.
					 */
					e.display.asyncExec(new Runnable() {
						public void run() {
							if (isValid()) {
								if (scrollbarClicked || hasFocus()) {
									return;
								}
								// Workaround a problem on X and Mac, whereby at
								// this point, the focus control is not known.
								// This can happen, for example, when resizing
								// the popup shell on the Mac.
								// Check the active shell.
								Shell activeShell = e.display.getActiveShell();
								if (activeShell == getShell()
										|| (infoPopup != null && infoPopup
												.getShell() == activeShell)) {
									return;
								}
								/*
								 * System.out.println(e);
								 * System.out.println(e.display.getFocusControl());
								 * System.out.println(e.display.getActiveShell());
								 */
								close();
							}
						}
					});
					return;
				}

				// Scroll bar has been clicked. Remember this for focus event
				// processing.
				if (e.type == SWT.Selection) {
					scrollbarClicked = true;
					return;
				}
				// For all other events, merely getting them dictates closure.
				/* STS Change: new code: */
				e.display.asyncExec(new Runnable() {
					public void run() {
						close(); 
					}
				});
				/* STS Change: old code: 
				close();
				*/
			}

			// Install the listeners for events that need to be monitored for
			// popup closure.
			void installListeners() {
				// Listeners on this popup's table and scroll bar
				proposalTable.addListener(SWT.FocusOut, this);
				ScrollBar scrollbar = proposalTable.getVerticalBar();
				if (scrollbar != null) {
					scrollbar.addListener(SWT.Selection, this);
				}

				// Listeners on this popup's shell
				getShell().addListener(SWT.Deactivate, this);
				getShell().addListener(SWT.Close, this);

				// Listeners on the target control
				control.addListener(SWT.MouseDoubleClick, this);
				control.addListener(SWT.MouseDown, this);
				control.addListener(SWT.Dispose, this);
				control.addListener(SWT.FocusOut, this);
				// Listeners on the target control's shell
				Shell controlShell = control.getShell();
				controlShell.addListener(SWT.Move, this);
				controlShell.addListener(SWT.Resize, this);

			}

			// Remove installed listeners
			void removeListeners() {
				if (isValid()) {
					proposalTable.removeListener(SWT.FocusOut, this);
					ScrollBar scrollbar = proposalTable.getVerticalBar();
					if (scrollbar != null) {
						scrollbar.removeListener(SWT.Selection, this);
					}

					getShell().removeListener(SWT.Deactivate, this);
					getShell().removeListener(SWT.Close, this);
				}

				if (control != null && !control.isDisposed()) {

					control.removeListener(SWT.MouseDoubleClick, this);
					control.removeListener(SWT.MouseDown, this);
					control.removeListener(SWT.Dispose, this);
					control.removeListener(SWT.FocusOut, this);

					Shell controlShell = control.getShell();
					controlShell.removeListener(SWT.Move, this);
					controlShell.removeListener(SWT.Resize, this);
				}
			}
		}

		/*
		 * The listener we will install on the target control.
		 */
		private final class TargetControlListener implements Listener {
			// Key events from the control
			public void handleEvent(Event e) {
				if (!isValid()) {
					return;
				}

				char key = e.character;

				// Traverse events are handled depending on whether the
				// event has a character.
				if (e.type == SWT.Traverse) {
					// If the traverse event contains a legitimate character,
					// then we must set doit false so that the widget will
					// receive the key event. We return immediately so that
					// the character is handled only in the key event.
					// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132101
					if (key != 0) {
						e.doit = false;
						return;
					}
					// Traversal does not contain a character. Set doit true
					// to indicate TRAVERSE_NONE will occur and that no key
					// event will be triggered. We will check for navigation
					// keys below.
					e.detail = SWT.TRAVERSE_NONE;
					e.doit = true;
				} else {
					// Default is to only propagate when configured that way.
					// Some keys will always set doit to false anyway.
					e.doit = propagateKeys;
				}

				// No character. Check for navigation keys.

				if (key == 0) {
					int newSelection = proposalTable.getSelectionIndex();
					int visibleRows = (proposalTable.getSize().y / proposalTable
							.getItemHeight()) - 1;
					switch (e.keyCode) {
					case SWT.ARROW_UP:
						newSelection -= 1;
						if (newSelection < 0) {
							newSelection = proposalTable.getItemCount() - 1;
						}
						// Not typical - usually we get this as a Traverse and
						// therefore it never propagates. Added for consistency.
						if (e.type == SWT.KeyDown) {
							// don't propagate to control
							e.doit = false;
						}

						break;

					case SWT.ARROW_DOWN:
						newSelection += 1;
						if (newSelection > proposalTable.getItemCount() - 1) {
							newSelection = 0;
						}
						// Not typical - usually we get this as a Traverse and
						// therefore it never propagates. Added for consistency.
						if (e.type == SWT.KeyDown) {
							// don't propagate to control
							e.doit = false;
						}

						break;

					case SWT.PAGE_DOWN:
						newSelection += visibleRows;
						if (newSelection >= proposalTable.getItemCount()) {
							newSelection = proposalTable.getItemCount() - 1;
						}
						if (e.type == SWT.KeyDown) {
							// don't propagate to control
							e.doit = false;
						}
						break;

					case SWT.PAGE_UP:
						newSelection -= visibleRows;
						if (newSelection < 0) {
							newSelection = 0;
						}
						if (e.type == SWT.KeyDown) {
							// don't propagate to control
							e.doit = false;
						}
						break;

					case SWT.HOME:
						newSelection = 0;
						if (e.type == SWT.KeyDown) {
							// don't propagate to control
							e.doit = false;
						}
						break;

					case SWT.END:
						newSelection = proposalTable.getItemCount() - 1;
						if (e.type == SWT.KeyDown) {
							// don't propagate to control
							e.doit = false;
						}
						break;

					// If received as a Traverse, these should propagate
					// to the control as keydown. If received as a keydown,
					// proposals should be recomputed since the cursor
					// position has changed.
					case SWT.ARROW_LEFT:
					case SWT.ARROW_RIGHT:
						if (e.type == SWT.Traverse) {
							e.doit = false;
						} else {
							e.doit = true;
							String contents = getControlContentAdapter()
									.getControlContents(getControl());
							// If there are no contents, changes in cursor
							// position have no effect. Note also that we do 
							// not affect the filter text on ARROW_LEFT as 
							// we would with BS.
							if (contents.length() > 0) {
								asyncRecomputeProposals(filterText);
							}
						}
						break;

					// Any unknown keycodes will cause the popup to close.
					// Modifier keys are explicitly checked and ignored because
					// they are not complete yet (no character).
					default:
						if (e.keyCode != SWT.CAPS_LOCK && e.keyCode != SWT.NUM_LOCK 
								&& e.keyCode != SWT.MOD1
								&& e.keyCode != SWT.MOD2
								&& e.keyCode != SWT.MOD3
								&& e.keyCode != SWT.MOD4) {
							close();
						}
						return;
					}

					// If any of these navigation events caused a new selection,
					// then handle that now and return.
					if (newSelection >= 0) {
						selectProposal(newSelection);
					}
					return;
				}

				// key != 0
				// Check for special keys involved in cancelling, accepting, or
				// filtering the proposals.
				switch (key) {
				case SWT.ESC:
					e.doit = false;
					close();
					break;

				case SWT.LF:
				case SWT.CR:
					e.doit = false;
					Object p = getSelectedProposal();
					if (p != null) {
						acceptCurrentProposal();
					} else {
						close();
					}
					break;

				case SWT.TAB:
					e.doit = false;
					getShell().setFocus();
					return;

				case SWT.BS:
					// Backspace should back out of any stored filter text
					if (filterStyle != FILTER_NONE) {
						// We have no filter to back out of, so do nothing
						if (filterText.length() == 0) {
							return;
						}
						// There is filter to back out of
						filterText = filterText.substring(0, filterText
								.length() - 1);
						asyncRecomputeProposals(filterText);
						return;
					}
					// There is no filtering provided by us, but some
					// clients provide their own filtering based on content.
					// Recompute the proposals if the cursor position
					// will change (is not at 0).
					int pos = getControlContentAdapter().getCursorPosition(
							getControl());
					// We rely on the fact that the contents and pos do not yet
					// reflect the result of the BS. If the contents were
					// already empty, then BS should not cause
					// a recompute.
					if (pos > 0) {
						asyncRecomputeProposals(filterText);
					}
					break;

				default:
					// If the key is a defined unicode character, and not one of
					// the special cases processed above, update the filter text
					// and filter the proposals.
					if (Character.isDefined(key)) {
						if (filterStyle == FILTER_CUMULATIVE) {
							filterText = filterText + String.valueOf(key);
						} else if (filterStyle == FILTER_CHARACTER) {
							filterText = String.valueOf(key);
						}
						// Recompute proposals after processing this event.
						asyncRecomputeProposals(filterText);
					}
					break;
				}
			}
		}

		/*
		 * Internal class used to implement the secondary popup.
		 */
		private class InfoPopupDialog extends PopupDialog {

			/*
			 * The text control that displays the text.
			 */
			private Text text;

			/*
			 * The String shown in the popup.
			 */
			private String contents = EMPTY;

			/*
			 * Construct an info-popup with the specified parent.
			 */
			InfoPopupDialog(Shell parent) {
				super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false, false,
						false, false, null, null);
			}

			/*
			 * Create a text control for showing the info about a proposal.
			 */
			protected Control createDialogArea(Composite parent) {
				text = new Text(parent, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP
						| SWT.NO_FOCUS);

				// Use the compact margins employed by PopupDialog.
				GridData gd = new GridData(GridData.BEGINNING
						| GridData.FILL_BOTH);
				gd.horizontalIndent = PopupDialog.POPUP_HORIZONTALSPACING;
				gd.verticalIndent = PopupDialog.POPUP_VERTICALSPACING;
				text.setLayoutData(gd);
				text.setText(contents);

				// since SWT.NO_FOCUS is only a hint...
				text.addFocusListener(new FocusAdapter() {
					public void focusGained(FocusEvent event) {
						ContentProposalPopup.this.close();
					}
				});
				return text;
			}

			/*
			 * Adjust the bounds so that we appear adjacent to our parent shell
			 */
			protected void adjustBounds() {
				Rectangle parentBounds = getParentShell().getBounds();
				Rectangle proposedBounds;
				// Try placing the info popup to the right
				Rectangle rightProposedBounds = new Rectangle(parentBounds.x
						+ parentBounds.width
						+ PopupDialog.POPUP_HORIZONTALSPACING, parentBounds.y
						+ PopupDialog.POPUP_VERTICALSPACING,
						parentBounds.width, parentBounds.height);
				rightProposedBounds = getConstrainedShellBounds(rightProposedBounds);
				// If it won't fit on the right, try the left
				if (rightProposedBounds.intersects(parentBounds)) {
					Rectangle leftProposedBounds = new Rectangle(parentBounds.x
							- parentBounds.width - POPUP_HORIZONTALSPACING - 1,
							parentBounds.y, parentBounds.width,
							parentBounds.height);
					leftProposedBounds = getConstrainedShellBounds(leftProposedBounds);
					// If it won't fit on the left, choose the proposed bounds
					// that fits the best
					if (leftProposedBounds.intersects(parentBounds)) {
						if (rightProposedBounds.x - parentBounds.x >= parentBounds.x
								- leftProposedBounds.x) {
							rightProposedBounds.x = parentBounds.x
									+ parentBounds.width
									+ PopupDialog.POPUP_HORIZONTALSPACING;
							proposedBounds = rightProposedBounds;
						} else {
							leftProposedBounds.width = parentBounds.x
									- POPUP_HORIZONTALSPACING
									- leftProposedBounds.x;
							proposedBounds = leftProposedBounds;
						}
					} else {
						// use the proposed bounds on the left
						proposedBounds = leftProposedBounds;
					}
				} else {
					// use the proposed bounds on the right
					proposedBounds = rightProposedBounds;
				}
				getShell().setBounds(proposedBounds);
			}
			
			/*
			 * (non-Javadoc)
			 * @see org.eclipse.jface.dialogs.PopupDialog#getForeground()
			 */
			protected Color getForeground() {
				return control.getDisplay().
						getSystemColor(SWT.COLOR_INFO_FOREGROUND);
			}
			
			/*
			 * (non-Javadoc)
			 * @see org.eclipse.jface.dialogs.PopupDialog#getBackground()
			 */
			protected Color getBackground() {
				return control.getDisplay().
						getSystemColor(SWT.COLOR_INFO_BACKGROUND);
			}

			/*
			 * Set the text contents of the popup.
			 */
			void setContents(String newContents) {
				if (newContents == null) {
					newContents = EMPTY;
				}
				this.contents = newContents;
				if (text != null && !text.isDisposed()) {
					text.setText(contents);
				}
			}

			/*
			 * Return whether the popup has focus.
			 */
			boolean hasFocus() {
				if (text == null || text.isDisposed()) {
					return false;
				}
				return text.getShell().isFocusControl()
						|| text.isFocusControl();
			}
		}

		/*
		 * The listener installed on the target control.
		 */
		private Listener targetControlListener;

		/*
		 * The listener installed in order to close the popup.
		 */
		private PopupCloserListener popupCloser;

		/*
		 * The table used to show the list of proposals.
		 */
		private Table proposalTable;

		/*
		 * The proposals to be shown (cached to avoid repeated requests).
		 */
		private IContentProposal[] proposals;

		/*
		 * Secondary popup used to show detailed information about the selected
		 * proposal..
		 */
		private InfoPopupDialog infoPopup;

		/*
		 * Flag indicating whether there is a pending secondary popup update.
		 */
		private boolean pendingDescriptionUpdate = false;

		/*
		 * Filter text - tracked while popup is open, only if we are told to
		 * filter
		 */
		private String filterText = EMPTY;

		/**
		 * Constructs a new instance of this popup, specifying the control for
		 * which this popup is showing content, and how the proposals should be
		 * obtained and displayed.
		 * 
		 * @param infoText
		 *           Text to be shown in a lower info area, or
		 *           <code>null</code> if there is no info area.
		 */
		ContentProposalPopup(String infoText, IContentProposal[] proposals) {
			// IMPORTANT: Use of SWT.ON_TOP is critical here for ensuring
			// that the target control retains focus on Mac and Linux. Without
			// it, the focus will disappear, keystrokes will not go to the
			// popup, and the popup closer will wrongly close the popup.
			// On platforms where SWT.ON_TOP overrides SWT.RESIZE, we will live
			// with this.
			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=126138
			super(control.getShell(), SWT.RESIZE | SWT.ON_TOP, false, false, false,
					false, false, null, infoText);
			this.proposals = proposals;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.jface.dialogs.PopupDialog#getForeground()
		 */
		protected Color getForeground() {
			return JFaceResources.getColorRegistry().get(
					JFacePreferences.CONTENT_ASSIST_FOREGROUND_COLOR);
		}
		
		/*
		 * (non-Javadoc)
		 * @see org.eclipse.jface.dialogs.PopupDialog#getBackground()
		 */
		protected Color getBackground() {
			return JFaceResources.getColorRegistry().get(
					JFacePreferences.CONTENT_ASSIST_BACKGROUND_COLOR);
		}

		/*
		 * Creates the content area for the proposal popup. This creates a table
		 * and places it inside the composite. The table will contain a list of
		 * all the proposals.
		 * 
		 * @param parent The parent composite to contain the dialog area; must
		 * not be <code>null</code>.
		 */
		protected final Control createDialogArea(final Composite parent) {
			// Use virtual where appropriate (see flag definition).
			if (USE_VIRTUAL) {
				proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL
						| SWT.VIRTUAL);

				Listener listener = new Listener() {
					public void handleEvent(Event event) {
						handleSetData(event);
					}
				};
				proposalTable.addListener(SWT.SetData, listener);
			} else {
				proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL);
			}

			// set the proposals to force population of the table.
			setProposals(filterProposals(proposals, filterText));

			proposalTable.setHeaderVisible(false);
			proposalTable.addSelectionListener(new SelectionListener() {

				public void widgetSelected(SelectionEvent e) {
					// If a proposal has been selected, show it in the secondary
					// popup. Otherwise close the popup.
					if (e.item == null) {
						if (infoPopup != null) {
							infoPopup.close();
						}
					} else {
						showProposalDescription();
					}
				}

				// Default selection was made. Accept the current proposal.
				public void widgetDefaultSelected(SelectionEvent e) {
					acceptCurrentProposal();
				}
			});
			return proposalTable;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jface.dialogs.PopupDialog.adjustBounds()
		 */
		protected void adjustBounds() {
			// Get our control's location in display coordinates.
			Point location = control.getDisplay().map(control.getParent(), null, control.getLocation());			
			int initialX = location.x + POPUP_OFFSET;
			int initialY = location.y + control.getSize().y + POPUP_OFFSET;
			// If we are inserting content, use the cursor position to
			// position the control.
			if (getProposalAcceptanceStyle() == PROPOSAL_INSERT) {
				Rectangle insertionBounds = controlContentAdapter
						.getInsertionBounds(control);
				initialX = initialX + insertionBounds.x;
				initialY = location.y + insertionBounds.y
						+ insertionBounds.height;
			}

			// If there is no specified size, force it by setting
			// up a layout on the table.
			if (popupSize == null) {
				GridData data = new GridData(GridData.FILL_BOTH);
				data.heightHint = proposalTable.getItemHeight()
						* POPUP_CHAR_HEIGHT;
				data.widthHint = Math.max(control.getSize().x,
						POPUP_MINIMUM_WIDTH);
				proposalTable.setLayoutData(data);
				getShell().pack();
				popupSize = getShell().getSize();
			}
			
			// Constrain to the display
			Rectangle constrainedBounds = getConstrainedShellBounds(new Rectangle(initialX, initialY, popupSize.x, popupSize.y));
			
			// If there has been an adjustment causing the popup to overlap 
			// with the control, then put the popup above the control.
			if (constrainedBounds.y < initialY)
				getShell().setBounds(initialX, location.y - popupSize.y, popupSize.x, popupSize.y);
			else
				getShell().setBounds(initialX, initialY, popupSize.x, popupSize.y);

			// Now set up a listener to monitor any changes in size.
			getShell().addListener(SWT.Resize, new Listener() {
				public void handleEvent(Event e) {
					popupSize = getShell().getSize();
					if (infoPopup != null) {
						infoPopup.adjustBounds();
					}
				}
			});
		}

		/*
		 * Handle the set data event. Set the item data of the requested item to
		 * the corresponding proposal in the proposal cache.
		 */
		private void handleSetData(Event event) {
			TableItem item = (TableItem) event.item;
			int index = proposalTable.indexOf(item);

			if (0 <= index && index < proposals.length) {
				IContentProposal current = proposals[index];
				item.setText(getString(current));
				item.setImage(getImage(current));
				item.setData(current);
			} else {
				// this should not happen, but does on win32
			}
		}

		/*
		 * Caches the specified proposals and repopulates the table if it has
		 * been created.
		 */
		private void setProposals(IContentProposal[] newProposals) {
			if (newProposals == null || newProposals.length == 0) {
				newProposals = getEmptyProposalArray();
			}
			this.proposals = newProposals;

			// If there is a table
			if (isValid()) {
				final int newSize = newProposals.length;
				if (USE_VIRTUAL) {
					// Set and clear the virtual table. Data will be
					// provided in the SWT.SetData event handler.
					proposalTable.setItemCount(newSize);
					proposalTable.clearAll();
				} else {
					// Populate the table manually
					proposalTable.setRedraw(false);
					proposalTable.setItemCount(newSize);
					TableItem[] items = proposalTable.getItems();
					for (int i = 0; i < items.length; i++) {
						TableItem item = items[i];
						IContentProposal proposal = newProposals[i];
						item.setText(getString(proposal));
						item.setImage(getImage(proposal));
						item.setData(proposal);
					}
					proposalTable.setRedraw(true);
				}
				// Default to the first selection if there is content.
				if (newProposals.length > 0) {
					selectProposal(0);
				} else {
					// No selection, close the secondary popup if it was open
					if (infoPopup != null) {
						infoPopup.close();
					}

				}
			}
		}

		/*
		 * Get the string for the specified proposal. Always return a String of
		 * some kind.
		 */
		private String getString(IContentProposal proposal) {
			if (proposal == null) {
				return EMPTY;
			}
			if (labelProvider == null) {
				return proposal.getLabel() == null ? proposal.getContent()
						: proposal.getLabel();
			}
			return labelProvider.getText(proposal);
		}

		/*
		 * Get the image for the specified proposal. If there is no image
		 * available, return null.
		 */
		private Image getImage(IContentProposal proposal) {
			if (proposal == null || labelProvider == null) {
				return null;
			}
			return labelProvider.getImage(proposal);
		}

		/*
		 * Return an empty array. Used so that something always shows in the
		 * proposal popup, even if no proposal provider was specified.
		 */
		private IContentProposal[] getEmptyProposalArray() {
			return new IContentProposal[0];
		}

		/*
		 * Answer true if the popup is valid, which means the table has been
		 * created and not disposed.
		 */
		private boolean isValid() {
			return proposalTable != null && !proposalTable.isDisposed();
		}

		/*
		 * Return whether the receiver has focus. Since 3.4, this includes a
		 * check for whether the info popup has focus.
		 */
		private boolean hasFocus() {
			if (!isValid()) {
				return false;
			}
			if (getShell().isFocusControl() || proposalTable.isFocusControl()) {
				return true;
			}
			if (infoPopup != null && infoPopup.hasFocus()) {
				return true;
			}
			return false;
		}

		/*
		 * Return the current selected proposal.
		 */
		private IContentProposal getSelectedProposal() {
			if (isValid()) {
				int i = proposalTable.getSelectionIndex();
				if (proposals == null || i < 0 || i >= proposals.length) {
					return null;
				}
				return proposals[i];
			}
			return null;
		}

		/*
		 * Select the proposal at the given index.
		 */
		private void selectProposal(int index) {
			Assert
					.isTrue(index >= 0,
							"Proposal index should never be negative"); //$NON-NLS-1$
			if (!isValid() || proposals == null || index >= proposals.length) {
				return;
			}
			proposalTable.setSelection(index);
			proposalTable.showSelection();

			showProposalDescription();
		}

		/**
		 * Opens this ContentProposalPopup. This method is extended in order to
		 * add the control listener when the popup is opened and to invoke the
		 * secondary popup if applicable.
		 * 
		 * @return the return code
		 * 
		 * @see org.eclipse.jface.window.Window#open()
		 */
		public int open() {
			int value = super.open();
			if (popupCloser == null) {
				popupCloser = new PopupCloserListener();
			}
			popupCloser.installListeners();
			IContentProposal p = getSelectedProposal();
			if (p != null) {
				showProposalDescription();
			}
			return value;
		}

		/**
		 * Closes this popup. This method is extended to remove the control
		 * listener.
		 * 
		 * @return <code>true</code> if the window is (or was already) closed,
		 *        and <code>false</code> if it is still open
		 */
		public boolean close() {
			popupCloser.removeListeners();
			if (infoPopup != null) {
				infoPopup.close();
			}
			boolean ret = super.close();
			notifyPopupClosed();
			return ret;
		}

		/*
		 * Show the currently selected proposal's description in a secondary
		 * popup.
		 */
		private void showProposalDescription() {
			// If we do not already have a pending update, then
			// create a thread now that will show the proposal description
			if (!pendingDescriptionUpdate) {
				// Create a thread that will sleep for the specified delay
				// before creating the popup. We do not use Jobs since this
				// code must be able to run independently of the Eclipse
				// runtime.
				Runnable runnable = new Runnable() {
					public void run() {
						pendingDescriptionUpdate = true;
						try {
							Thread.sleep(POPUP_DELAY);
						} catch (InterruptedException e) {
						}
						if (!isValid()) {
							return;
						}
						getShell().getDisplay().syncExec(new Runnable() {
							public void run() {
								// Query the current selection since we have
								// been delayed
								IContentProposal p = getSelectedProposal();
								if (p != null) {
									String description = p.getDescription();
									if (description != null) {
										if (infoPopup == null) {
											infoPopup = new InfoPopupDialog(
													getShell());
											infoPopup.open();
											infoPopup
													.getShell()
													.addDisposeListener(
															new DisposeListener() {
																public void widgetDisposed(
																		DisposeEvent event) {
																	infoPopup = null;
																}
															});
										}
										infoPopup.setContents(p
												.getDescription());
									} else if (infoPopup != null) {
										infoPopup.close();
									}
									pendingDescriptionUpdate = false;

								}
							}
						});
					}
				};
				Thread t = new Thread(runnable);
				t.start();
			}
		}

		/*
		 * Accept the current proposal.
		 */
		private void acceptCurrentProposal() {
			// Close before accepting the proposal. This is important
			// so that the cursor position can be properly restored at
			// acceptance, which does not work without focus on some controls.
			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
			IContentProposal proposal = getSelectedProposal();
			close();
			proposalAccepted(proposal);
		}

		/*
		 * Request the proposals from the proposal provider, and recompute any
		 * caches. Repopulate the popup if it is open.
		 */
		private void recomputeProposals(String filterText) {
			IContentProposal[] allProposals = getProposals();
			if (allProposals == null)
				 allProposals = getEmptyProposalArray();
			// If the non-filtered proposal list is empty, we should
			// close the popup.
			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
			if (allProposals.length == 0) {
				proposals = allProposals;
				close();
			} else {
				// Keep the popup open, but filter by any provided filter text
				setProposals(filterProposals(allProposals, filterText));
			}
		}

		/*
		 * In an async block, request the proposals. This is used when clients
		 * are in the middle of processing an event that affects the widget
		 * content. By using an async, we ensure that the widget content is up
		 * to date with the event.
		 */
		private void asyncRecomputeProposals(final String filterText) {
			if (isValid()) {
				control.getDisplay().asyncExec(new Runnable() {
					public void run() {
						recordCursorPosition();
						recomputeProposals(filterText);
					}
				});
			} else {
				recomputeProposals(filterText);
			}
		}

		/*
		 * Filter the provided list of content proposals according to the filter
		 * text.
		 */
		private IContentProposal[] filterProposals(
				IContentProposal[] proposals, String filterString) {
			if (filterString.length() == 0) {
				return proposals;
			}

			// Check each string for a match. Use the string displayed to the
			// user, not the proposal content.
			ArrayList list = new ArrayList();
			for (int i = 0; i < proposals.length; i++) {
				String string = getString(proposals[i]);
				if (string.length() >= filterString.length()
						&& string.substring(0, filterString.length())
								.equalsIgnoreCase(filterString)) {
					list.add(proposals[i]);
				}

			}
			return (IContentProposal[]) list.toArray(new IContentProposal[list
					.size()]);
		}

		Listener getTargetControlListener() {
			if (targetControlListener == null) {
				targetControlListener = new TargetControlListener();
			}
			return targetControlListener;
		}
	}

	/**
	 * Flag that controls the printing of debug info.
	 */
	public static final boolean DEBUG = false;

	/**
	 * Indicates that a chosen proposal should be inserted into the field.
	 */
	public static final int PROPOSAL_INSERT = 1;

	/**
	 * Indicates that a chosen proposal should replace the entire contents of
	 * the field.
	 */
	public static final int PROPOSAL_REPLACE = 2;

	/**
	 * Indicates that the contents of the control should not be modified when a
	 * proposal is chosen. This is typically used when a client needs more
	 * specialized behavior when a proposal is chosen. In this case, clients
	 * typically register an IContentProposalListener so that they are notified
	 * when a proposal is chosen.
	 */
	public static final int PROPOSAL_IGNORE = 3;

	/**
	 * Indicates that there should be no filter applied as keys are typed in the
	 * popup.
	 */
	public static final int FILTER_NONE = 1;

	/**
	 * Indicates that a single character filter applies as keys are typed in the
	 * popup.
	 */
	public static final int FILTER_CHARACTER = 2;

	/**
	 * Indicates that a cumulative filter applies as keys are typed in the
	 * popup. That is, each character typed will be added to the filter.
	 * 
	 * @deprecated As of 3.4, filtering that is sensitive to changes in the
	 *            control content should be performed by the supplied
	 *            {@link IContentProposalProvider}, such as that performed by
	 *            {@link SimpleContentProposalProvider}
	 */
	public static final int FILTER_CUMULATIVE = 3;

	/*
	 * Set to <code>true</code> to use a Table with SWT.VIRTUAL. This is a
	 * workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=98585#c40
	 * The corresponding SWT bug is
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=90321
	 */
	private static final boolean USE_VIRTUAL = !Util.isMotif();

	/*
	 * The delay before showing a secondary popup.
	 */
	private static final int POPUP_DELAY = 750;

	/*
	 * The character height hint for the popup. May be overridden by using
	 * setInitialPopupSize.
	 */
	private static final int POPUP_CHAR_HEIGHT = 10;

	/*
	 * The minimum pixel width for the popup. May be overridden by using
	 * setInitialPopupSize.
	 */
	private static final int POPUP_MINIMUM_WIDTH = 300;

	/*
	 * The pixel offset of the popup from the bottom corner of the control.
	 */
	private static final int POPUP_OFFSET = 3;

	/*
	 * Empty string.
	 */
	private static final String EMPTY = ""; //$NON-NLS-1$

	/*
	 * The object that provides content proposals.
	 */
	private IContentProposalProvider proposalProvider;

	/*
	 * A label provider used to display proposals in the popup, and to extract
	 * Strings from non-String proposals.
	 */
	private ILabelProvider labelProvider;

	/*
	 * The control for which content proposals are provided.
	 */
	private Control control;

	/*
	 * The adapter used to extract the String contents from an arbitrary
	 * control.
	 */
	private IControlContentAdapter controlContentAdapter;

	/*
	 * The popup used to show proposals.
	 */
	private ContentProposalPopup popup;

	/*
	 * The keystroke that signifies content proposals should be shown.
	 */
	private KeyStroke triggerKeyStroke;

	/*
	 * The String containing characters that auto-activate the popup.
	 */
	private String autoActivateString;

	/*
	 * Integer that indicates how an accepted proposal should affect the
	 * control. One of PROPOSAL_IGNORE, PROPOSAL_INSERT, or PROPOSAL_REPLACE.
	 * Default value is PROPOSAL_INSERT.
	 */
	private int proposalAcceptanceStyle = PROPOSAL_INSERT;

	/*
	 * A boolean that indicates whether key events received while the proposal
	 * popup is open should also be propagated to the control. Default value is
	 * true.
	 */
	private boolean propagateKeys = true;

	/*
	 * Integer that indicates the filtering style. One of FILTER_CHARACTER,
	 * FILTER_CUMULATIVE, FILTER_NONE.
	 */
	private int filterStyle = FILTER_NONE;

	/*
	 * The listener we install on the control.
	 */
	private Listener controlListener;

	/*
	 * The list of IContentProposalListener listeners.
	 */
	private ListenerList proposalListeners = new ListenerList();

	/*
	 * The list of IContentProposalListener2 listeners.
	 */
	private ListenerList proposalListeners2 = new ListenerList();

	/*
	 * Flag that indicates whether the adapter is enabled. In some cases,
	 * adapters may be installed but depend upon outside state.
	 */
	private boolean isEnabled = true;

	/*
	 * The delay in milliseconds used when autoactivating the popup.
	 */
	private int autoActivationDelay = 0;

	/*
	 * A boolean indicating whether a keystroke has been received. Used to see
	 * if an autoactivation delay was interrupted by a keystroke.
	 */
	private boolean receivedKeyDown;

	/*
	 * The desired size in pixels of the proposal popup.
	 */
	private Point popupSize;

	/*
	 * The remembered position of the insertion position. Not all controls will
	 * restore the insertion position if the proposal popup gets focus, so we
	 * need to remember it.
	 */
	private int insertionPos = -1;

	/*
	 * The remembered selection range. Not all controls will restore the
	 * selection position if the proposal popup gets focus, so we need to
	 * remember it.
	 */
	private Point selectionRange = new Point(-1, -1);

	/*
	 * A flag that indicates that we are watching modify events
	 */
	private boolean watchModify = false;

	/**
	 * Construct a content proposal adapter that can assist the user with
	 * choosing content for the field.
	 * 
	 * @param control
	 *           the control for which the adapter is providing content assist.
	 *           May not be <code>null</code>.
	 * @param controlContentAdapter
	 *           the <code>IControlContentAdapter</code> used to obtain and
	 *           update the control's contents as proposals are accepted. May
	 *           not be <code>null</code>.
	 * @param proposalProvider
	 *           the <code>IContentProposalProvider</code> used to obtain
	 *           content proposals for this control, or <code>null</code> if
	 *           no content proposal is available.
	 * @param keyStroke
	 *           the keystroke that will invoke the content proposal popup. If
	 *           this value is <code>null</code>, then proposals will be
	 *           activated automatically when any of the auto activation
	 *           characters are typed.
	 * @param autoActivationCharacters
	 *           An array of characters that trigger auto-activation of content
	 *           proposal. If specified, these characters will trigger
	 *           auto-activation of the proposal popup, regardless of whether
	 *           an explicit invocation keyStroke was specified. If this
	 *           parameter is <code>null</code>, then only a specified
	 *           keyStroke will invoke content proposal. If this parameter is
	 *           <code>null</code> and the keyStroke parameter is
	 *           <code>null</code>, then all alphanumeric characters will
	 *           auto-activate content proposal.
	 */
	public ContentProposalAdapter(Control control,
			IControlContentAdapter controlContentAdapter,
			IContentProposalProvider proposalProvider, KeyStroke keyStroke,
			char[] autoActivationCharacters) {
		super();
		// We always assume the control and content adapter are valid.
		Assert.isNotNull(control);
		Assert.isNotNull(controlContentAdapter);
		this.control = control;
		this.controlContentAdapter = controlContentAdapter;

		// The rest of these may be null
		this.proposalProvider = proposalProvider;
		this.triggerKeyStroke = keyStroke;
		if (autoActivationCharacters != null) {
			this.autoActivateString = new String(autoActivationCharacters);
		}
		addControlListener(control);
	}

	/**
	 * Get the control on which the content proposal adapter is installed.
	 * 
	 * @return the control on which the proposal adapter is installed.
	 */
	public Control getControl() {
		return control;
	}

	/**
	 * Get the label provider that is used to show proposals.
	 * 
	 * @return the {@link ILabelProvider} used to show proposals, or
	 *        <code>null</code> if one has not been installed.
	 */
	public ILabelProvider getLabelProvider() {
		return labelProvider;
	}

	/**
	 * Return a boolean indicating whether the receiver is enabled.
	 * 
	 * @return <code>true</code> if the adapter is enabled, and
	 *        <code>false</code> if it is not.
	 */
	public boolean isEnabled() {
		return isEnabled;
	}

	/**
	 * Set the label provider that is used to show proposals. The lifecycle of
	 * the specified label provider is not managed by this adapter. Clients must
	 * dispose the label provider when it is no longer needed.
	 * 
	 * @param labelProvider
	 *           the (@link ILabelProvider} used to show proposals.
	 */
	public void setLabelProvider(ILabelProvider labelProvider) {
		this.labelProvider = labelProvider;
	}

	/**
	 * Return the proposal provider that provides content proposals given the
	 * current content of the field. A value of <code>null</code> indicates
	 * that there are no content proposals available for the field.
	 * 
	 * @return the {@link IContentProposalProvider} used to show proposals. May
	 *        be <code>null</code>.
	 */
	public IContentProposalProvider getContentProposalProvider() {
		return proposalProvider;
	}

	/**
	 * Set the content proposal provider that is used to show proposals.
	 * 
	 * @param proposalProvider
	 *           the {@link IContentProposalProvider} used to show proposals
	 */
	public void setContentProposalProvider(
			IContentProposalProvider proposalProvider) {
		this.proposalProvider = proposalProvider;
	}

	/**
	 * Return the array of characters on which the popup is autoactivated.
	 * 
	 * @return An array of characters that trigger auto-activation of content
	 *        proposal. If specified, these characters will trigger
	 *        auto-activation of the proposal popup, regardless of whether an
	 *        explicit invocation keyStroke was specified. If this parameter is
	 *        <code>null</code>, then only a specified keyStroke will invoke
	 *        content proposal. If this value is <code>null</code> and the
	 *        keyStroke value is <code>null</code>, then all alphanumeric
	 *        characters will auto-activate content proposal.
	 */
	public char[] getAutoActivationCharacters() {
		if (autoActivateString == null) {
			return null;
		}
		return autoActivateString.toCharArray();
	}

	/**
	 * Set the array of characters that will trigger autoactivation of the
	 * popup.
	 * 
	 * @param autoActivationCharacters
	 *           An array of characters that trigger auto-activation of content
	 *           proposal. If specified, these characters will trigger
	 *           auto-activation of the proposal popup, regardless of whether
	 *           an explicit invocation keyStroke was specified. If this
	 *           parameter is <code>null</code>, then only a specified
	 *           keyStroke will invoke content proposal. If this parameter is
	 *           <code>null</code> and the keyStroke value is
	 *           <code>null</code>, then all alphanumeric characters will
	 *           auto-activate content proposal.
	 * 
	 */
	public void setAutoActivationCharacters(char[] autoActivationCharacters) {
		if (autoActivationCharacters == null) {
			this.autoActivateString = null;
		} else {
			this.autoActivateString = new String(autoActivationCharacters);
		}
	}

	/**
	 * Set the delay, in milliseconds, used before any autoactivation is
	 * triggered.
	 * 
	 * @return the time in milliseconds that will pass before a popup is
	 *        automatically opened
	 */
	public int getAutoActivationDelay() {
		return autoActivationDelay;

	}

	/**
	 * Set the delay, in milliseconds, used before autoactivation is triggered.
	 * 
	 * @param delay
	 *           the time in milliseconds that will pass before a popup is
	 *           automatically opened
	 */
	public void setAutoActivationDelay(int delay) {
		autoActivationDelay = delay;

	}

	/**
	 * Get the integer style that indicates how an accepted proposal affects the
	 * control's content.
	 * 
	 * @return a constant indicating how an accepted proposal should affect the
	 *        control's content. Should be one of <code>PROPOSAL_INSERT</code>,
	 *        <code>PROPOSAL_REPLACE</code>, or <code>PROPOSAL_IGNORE</code>.
	 *        (Default is <code>PROPOSAL_INSERT</code>).
	 */
	public int getProposalAcceptanceStyle() {
		return proposalAcceptanceStyle;
	}

	/**
	 * Set the integer style that indicates how an accepted proposal affects the
	 * control's content.
	 * 
	 * @param acceptance
	 *           a constant indicating how an accepted proposal should affect
	 *           the control's content. Should be one of
	 *           <code>PROPOSAL_INSERT</code>, <code>PROPOSAL_REPLACE</code>,
	 *           or <code>PROPOSAL_IGNORE</code>
	 */
	public void setProposalAcceptanceStyle(int acceptance) {
		proposalAcceptanceStyle = acceptance;
	}

	/**
	 * Return the integer style that indicates how keystrokes affect the content
	 * of the proposal popup while it is open.
	 * 
	 * @return a constant indicating how keystrokes in the proposal popup affect
	 *        filtering of the proposals shown. <code>FILTER_NONE</code>
	 *        specifies that no filtering will occur in the content proposal
	 *        list as keys are typed. <code>FILTER_CHARACTER</code> specifies
	 *        the content of the popup will be filtered by the most recently
	 *        typed character. <code>FILTER_CUMULATIVE</code> is deprecated
	 *        and no longer recommended. It specifies that the content of the
	 *        popup will be filtered by a string containing all the characters
	 *        typed since the popup has been open. The default is
	 *        <code>FILTER_NONE</code>.
	 */
	public int getFilterStyle() {
		return filterStyle;
	}

	/**
	 * Set the integer style that indicates how keystrokes affect the content of
	 * the proposal popup while it is open. Popup-based filtering is useful for
	 * narrowing and navigating the list of proposals provided once the popup is
	 * open. Filtering of the proposals will occur even when the control content
	 * is not affected by user typing. Note that automatic filtering is not used
	 * to achieve content-sensitive filtering such as auto-completion. Filtering
	 * that is sensitive to changes in the control content should be performed
	 * by the supplied {@link IContentProposalProvider}.
	 * 
	 * @param filterStyle
	 *           a constant indicating how keystrokes received in the proposal
	 *           popup affect filtering of the proposals shown.
	 *           <code>FILTER_NONE</code> specifies that no automatic
	 *           filtering of the content proposal list will occur as keys are
	 *           typed in the popup. <code>FILTER_CHARACTER</code> specifies
	 *           that the content of the popup will be filtered by the most
	 *           recently typed character. <code>FILTER_CUMULATIVE</code> is
	 *           deprecated and no longer recommended. It specifies that the
	 *           content of the popup will be filtered by a string containing
	 *           all the characters typed since the popup has been open.
	 */
	public void setFilterStyle(int filterStyle) {
		this.filterStyle = filterStyle;
	}

	/**
	 * Return the size, in pixels, of the content proposal popup.
	 * 
	 * @return a Point specifying the last width and height, in pixels, of the
	 *        content proposal popup.
	 */
	public Point getPopupSize() {
		return popupSize;
	}

	/**
	 * Set the size, in pixels, of the content proposal popup. This size will be
	 * used the next time the content proposal popup is opened.
	 * 
	 * @param size
	 *           a Point specifying the desired width and height, in pixels, of
	 *           the content proposal popup.
	 */
	public void setPopupSize(Point size) {
		popupSize = size;
	}

	/**
	 * Get the boolean that indicates whether key events (including
	 * auto-activation characters) received by the content proposal popup should
	 * also be propagated to the adapted control when the proposal popup is
	 * open.
	 * 
	 * @return a boolean that indicates whether key events (including
	 *        auto-activation characters) should be propagated to the adapted
	 *        control when the proposal popup is open. Default value is
	 *        <code>true</code>.
	 */
	public boolean getPropagateKeys() {
		return propagateKeys;
	}

	/**
	 * Set the boolean that indicates whether key events (including
	 * auto-activation characters) received by the content proposal popup should
	 * also be propagated to the adapted control when the proposal popup is
	 * open.
	 * 
	 * @param propagateKeys
	 *           a boolean that indicates whether key events (including
	 *           auto-activation characters) should be propagated to the
	 *           adapted control when the proposal popup is open.
	 */
	public void setPropagateKeys(boolean propagateKeys) {
		this.propagateKeys = propagateKeys;
	}

	/**
	 * Return the content adapter that can get or retrieve the text contents
	 * from the adapter's control. This method is used when a client, such as a
	 * content proposal listener, needs to update the control's contents
	 * manually.
	 * 
	 * @return the {@link IControlContentAdapter} which can update the control
	 *        text.
	 */
	public IControlContentAdapter getControlContentAdapter() {
		return controlContentAdapter;
	}

	/**
	 * Set the boolean flag that determines whether the adapter is enabled.
	 * 
	 * @param enabled
	 *           <code>true</code> if the adapter is enabled and responding
	 *           to user input, <code>false</code> if it is ignoring user
	 *           input.
	 * 
	 */
	public void setEnabled(boolean enabled) {
		// If we are disabling it while it's proposing content, close the
		// content proposal popup.
		if (isEnabled && !enabled) {
			if (popup != null) {
				popup.close();
			}
		}
		isEnabled = enabled;
	}

	/**
	 * Add the specified listener to the list of content proposal listeners that
	 * are notified when content proposals are chosen.
	 * </p>
	 * 
	 * @param listener
	 *           the IContentProposalListener to be added as a listener. Must
	 *           not be <code>null</code>. If an attempt is made to register
	 *           an instance which is already registered with this instance,
	 *           this method has no effect.
	 * 
	 * @see org.eclipse.jface.fieldassist.IContentProposalListener
	 */
	public void addContentProposalListener(IContentProposalListener listener) {
		proposalListeners.add(listener);
	}

	/**
	 * Removes the specified listener from the list of content proposal
	 * listeners that are notified when content proposals are chosen.
	 * </p>
	 * 
	 * @param listener
	 *           the IContentProposalListener to be removed as a listener. Must
	 *           not be <code>null</code>. If the listener has not already
	 *           been registered, this method has no effect.
	 * 
	 * @since 3.3
	 * @see org.eclipse.jface.fieldassist.IContentProposalListener
	 */
	public void removeContentProposalListener(IContentProposalListener listener) {
		proposalListeners.remove(listener);
	}

	/**
	 * Add the specified listener to the list of content proposal listeners that
	 * are notified when a content proposal popup is opened or closed.
	 * </p>
	 * 
	 * @param listener
	 *           the IContentProposalListener2 to be added as a listener. Must
	 *           not be <code>null</code>. If an attempt is made to register
	 *           an instance which is already registered with this instance,
	 *           this method has no effect.
	 * 
	 * @since 3.3
	 * @see org.eclipse.jface.fieldassist.IContentProposalListener2
	 */
	public void addContentProposalListener(IContentProposalListener2 listener) {
		proposalListeners2.add(listener);
	}

	/**
	 * Remove the specified listener from the list of content proposal listeners
	 * that are notified when a content proposal popup is opened or closed.
	 * </p>
	 * 
	 * @param listener
	 *           the IContentProposalListener2 to be removed as a listener.
	 *           Must not be <code>null</code>. If the listener has not
	 *           already been registered, this method has no effect.
	 * 
	 * @since 3.3
	 * @see org.eclipse.jface.fieldassist.IContentProposalListener2
	 */
	public void removeContentProposalListener(IContentProposalListener2 listener) {
		proposalListeners2.remove(listener);
	}

	/*
	 * Add our listener to the control. Debug information to be left in until
	 * this support is stable on all platforms.
	 */
	private void addControlListener(Control control) {
		if (DEBUG) {
			System.out
					.println("ContentProposalListener#installControlListener()"); //$NON-NLS-1$
		}

		if (controlListener != null) {
			return;
		}
		controlListener = new Listener() {
			public void handleEvent(Event e) {
				if (!isEnabled) {
					return;
				}

				switch (e.type) {
				case SWT.Traverse:
				case SWT.KeyDown:
					if (DEBUG) {
						StringBuffer sb;
						if (e.type == SWT.Traverse) {
							sb = new StringBuffer("Traverse"); //$NON-NLS-1$
						} else {
							sb = new StringBuffer("KeyDown"); //$NON-NLS-1$
						}
						sb.append(" received by adapter"); //$NON-NLS-1$
						dump(sb.toString(), e);
					}
					// If the popup is open, it gets first shot at the
					// keystroke and should set the doit flags appropriately.
					if (popup != null) {
						popup.getTargetControlListener().handleEvent(e);
						if (DEBUG) {
							StringBuffer sb;
							if (e.type == SWT.Traverse) {
								sb = new StringBuffer("Traverse"); //$NON-NLS-1$
							} else {
								sb = new StringBuffer("KeyDown"); //$NON-NLS-1$
							}
							sb.append(" after being handled by popup"); //$NON-NLS-1$
							dump(sb.toString(), e);
						}
						// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633
						// If the popup is open and this is a valid character, we
						// want to watch for the modified text.
						if (propagateKeys && e.character != 0)
							watchModify = true;

						return;
					}

					// We were only listening to traverse events for the popup
					if (e.type == SWT.Traverse) {
						return;
					}

					// The popup is not open. We are looking at keydown events
					// for a trigger to open the popup.
					if (triggerKeyStroke != null) {
						// Either there are no modifiers for the trigger and we
						// check the character field...
						if ((triggerKeyStroke.getModifierKeys() == KeyStroke.NO_KEY && triggerKeyStroke
								.getNaturalKey() == e.character)
								||
								// ...or there are modifiers, in which case the
								// keycode and state must match
								(triggerKeyStroke.getNaturalKey() == e.keyCode && ((triggerKeyStroke
										.getModifierKeys() & e.stateMask) == triggerKeyStroke
										.getModifierKeys()))) {
							// We never propagate the keystroke for an explicit
							// keystroke invocation of the popup
							e.doit = false;
							openProposalPopup(false);
							return;
						}
					}
					/*
					 * The triggering keystroke was not invoked. If a character
					 * was typed, compare it to the autoactivation characters.
					 */
					if (e.character != 0) {
						if (autoActivateString != null) {
							if (autoActivateString.indexOf(e.character) >= 0) {
								autoActivate();
							} else {
								// No autoactivation occurred, so record the key
								// down as a means to interrupt any
								// autoactivation that is pending due to
								// autoactivation delay.
								receivedKeyDown = true;
								// watch the modify so we can close the popup in
								// cases where there is no longer a trigger
								// character in the content
								watchModify = true;
							}
						} else {
							// The autoactivate string is null. If the trigger
							// is also null, we want to act on any modification
							// to the content. Set a flag so we'll catch this
							// in the modify event.
							if (triggerKeyStroke == null) {
								watchModify = true;
							}
						}
					} else {
						// A non-character key has been pressed. Interrupt any
						// autoactivation that is pending due to autoactivation delay.
						receivedKeyDown = true;
					}
					break;


					// There are times when we want to monitor content changes
					// rather than individual keystrokes to determine whether
					// the popup should be closed or opened based on the entire
					// content of the control.
					// The watchModify flag ensures that we don't autoactivate if
					// the content change was caused by something other than typing.
					// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650
					case SWT.Modify:
						if (allowsAutoActivate() && watchModify) {
							if (DEBUG) {
								dump("Modify event triggers popup open or close", e); //$NON-NLS-1$
							}
							watchModify = false;
							// We are in autoactivation mode, either for specific
							// characters or for all characters. In either case, 
							// we should close the proposal popup when there is no
							// content in the control.
							if (isControlContentEmpty()) {
								// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633
								closeProposalPopup();
							} else {
								// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
								// Given that we will close the popup when there are
								// no valid proposals, we must consider reopening it on any
								// content change when there are no particular autoActivation
								// characters
								if (autoActivateString == null) {
									autoActivate();
								} else {
									// Autoactivation characters are defined, but this
									// modify event does not involve one of them.  See
									// if any of the autoactivation characters are left
									// in the content and close the popup if none remain.
									if (!shouldPopupRemainOpen())
										closeProposalPopup();
								}
							}
						}
						break;
				default:
					break;
				}
			}

			/**
			 * Dump the given events to "standard" output.
			 * 
			 * @param who
			 *           who is dumping the event
			 * @param e
			 *           the event
			 */
			private void dump(String who, Event e) {
				StringBuffer sb = new StringBuffer(
						"--- [ContentProposalAdapter]\n"); //$NON-NLS-1$
				sb.append(who);
				sb.append(" - e: keyCode=" + e.keyCode + hex(e.keyCode)); //$NON-NLS-1$
				sb.append("; character=" + e.character + hex(e.character)); //$NON-NLS-1$
				sb.append("; stateMask=" + e.stateMask + hex(e.stateMask)); //$NON-NLS-1$
				sb.append("; doit=" + e.doit); //$NON-NLS-1$
				sb.append("; detail=" + e.detail + hex(e.detail)); //$NON-NLS-1$
				sb.append("; widget=" + e.widget); //$NON-NLS-1$
				System.out.println(sb);
			}

			private String hex(int i) {
				return "[0x" + Integer.toHexString(i) + ']'; //$NON-NLS-1$
			}
		};
		control.addListener(SWT.KeyDown, controlListener);
		control.addListener(SWT.Traverse, controlListener);
		control.addListener(SWT.Modify, controlListener);

		if (DEBUG) {
			System.out
					.println("ContentProposalAdapter#installControlListener() - installed"); //$NON-NLS-1$
		}
	}

	/**
	 * Open the proposal popup and display the proposals provided by the
	 * proposal provider. If there are no proposals to be shown, do not show the
	 * popup. This method returns immediately. That is, it does not wait for the
	 * popup to open or a proposal to be selected.
	 * 
	 * @param autoActivated
	 *           a boolean indicating whether the popup was autoactivated. If
	 *           false, a beep will sound when no proposals can be shown.
	 */
	private void openProposalPopup(boolean autoActivated) {
		if (isValid()) {
			if (popup == null) {
				// Check whether there are any proposals to be shown.
				recordCursorPosition(); // must be done before getting proposals
				IContentProposal[] proposals = getProposals();
				if (proposals.length > 0) {
					if (DEBUG) {
						System.out.println("POPUP OPENED BY PRECEDING EVENT"); //$NON-NLS-1$
					}
					recordCursorPosition();
					popup = new ContentProposalPopup(null, proposals);
					popup.open();
					popup.getShell().addDisposeListener(new DisposeListener() {
						public void widgetDisposed(DisposeEvent event) {
							popup = null;
						}
					});
					internalPopupOpened();
					notifyPopupOpened();
				} else if (!autoActivated) {
					getControl().getDisplay().beep();
				}
			}
		}
	}

	/**
	 * Open the proposal popup and display the proposals provided by the
	 * proposal provider. This method returns immediately. That is, it does not
	 * wait for a proposal to be selected. This method is used by subclasses to
	 * explicitly invoke the opening of the popup. If there are no proposals to
	 * show, the popup will not open and a beep will be sounded.
	 */
	protected void openProposalPopup() {
		openProposalPopup(false);
	}

	/**
	 * Close the proposal popup without accepting a proposal. This method
	 * returns immediately, and has no effect if the proposal popup was not
	 * open. This method is used by subclasses to explicitly close the popup
	 * based on additional logic.
	 * 
	 * @since 3.3
	 */
	protected void closeProposalPopup() {
		if (popup != null) {
			popup.close();
		}
	}

	/*
	 * A content proposal has been accepted. Update the control contents
	 * accordingly and notify any listeners.
	 * 
	 * @param proposal the accepted proposal
	 */
	private void proposalAccepted(IContentProposal proposal) {
		switch (proposalAcceptanceStyle) {
		case (PROPOSAL_REPLACE):
			setControlContent(proposal.getContent(), proposal
					.getCursorPosition());
			break;
		case (PROPOSAL_INSERT):
			insertControlContent(proposal.getContent(), proposal
					.getCursorPosition());
			break;
		default:
			// do nothing. Typically a listener is installed to handle this in
			// a custom way.
			break;
		}

		// In all cases, notify listeners of an accepted proposal.
		notifyProposalAccepted(proposal);
	}

	/*
	 * Set the text content of the control to the specified text, setting the
	 * cursorPosition at the desired location within the new contents.
	 */
	private void setControlContent(String text, int cursorPosition) {
		if (isValid()) {
			// should already be false, but just in case.
			watchModify = false;
			controlContentAdapter.setControlContents(control, text,
					cursorPosition);
		}
	}

	/*
	 * Insert the specified text into the control content, setting the
	 * cursorPosition at the desired location within the new contents.
	 */
	private void insertControlContent(String text, int cursorPosition) {
		if (isValid()) {
			// should already be false, but just in case.
			watchModify = false;
			// Not all controls preserve their selection index when they lose
			// focus, so we must set it explicitly here to what it was before
			// the popup opened.
			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063
			if (controlContentAdapter instanceof IControlContentAdapter2
					&& selectionRange.x != -1) {
				((IControlContentAdapter2) controlContentAdapter).setSelection(
						control, selectionRange);
			} else if (insertionPos != -1) {
				controlContentAdapter.setCursorPosition(control, insertionPos);
			}
			controlContentAdapter.insertControlContents(control, text,
					cursorPosition);
		}
	}

	/*
	 * Check that the control and content adapter are valid.
	 */
	private boolean isValid() {
		return control != null && !control.isDisposed()
				&& controlContentAdapter != null;
	}

	/*
	 * Record the control's cursor position.
	 */
	private void recordCursorPosition() {
		if (isValid()) {
			IControlContentAdapter adapter = getControlContentAdapter();
			insertionPos = adapter.getCursorPosition(control);
			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063
			if (adapter instanceof IControlContentAdapter2) {
				selectionRange = ((IControlContentAdapter2) adapter)
						.getSelection(control);
			}

		}
	}

	/*
	 * Get the proposals from the proposal provider. Gets all of the proposals
	 * without doing any filtering.
	 */
	private IContentProposal[] getProposals() {
		if (proposalProvider == null || !isValid()) {
			return null;
		}
		if (DEBUG) {
			System.out.println(">>> obtaining proposals from provider"); //$NON-NLS-1$
		}
		int position = insertionPos;
		if (position == -1) {
			position = getControlContentAdapter().getCursorPosition(
					getControl());
		}
		String contents = getControlContentAdapter().getControlContents(
				getControl());
		IContentProposal[] proposals = proposalProvider.getProposals(contents,
				position);
		return proposals;
	}

	/**
	 * Autoactivation has been triggered. Open the popup using any specified
	 * delay.
	 */
	private void autoActivate() {
		if (autoActivationDelay > 0) {
			Runnable runnable = new Runnable() {
				public void run() {
					receivedKeyDown = false;
					try {
						Thread.sleep(autoActivationDelay);
					} catch (InterruptedException e) {
					}
					if (!isValid() || receivedKeyDown) {
						return;
					}
					getControl().getDisplay().syncExec(new Runnable() {
						public void run() {
							openProposalPopup(true);
						}
					});
				}
			};
			Thread t = new Thread(runnable);
			t.start();
		} else {
			// Since we do not sleep, we must open the popup
			// in an async exec. This is necessary because
			// this method may be called in the middle of handling
			// some event that will cause the cursor position or
			// other important info to change as a result of this
			// event occurring.
			getControl().getDisplay().asyncExec(new Runnable() {
				public void run() {
					if (isValid()) {
						openProposalPopup(true);
					}
				}
			});
		}
	}

	/*
	 * A proposal has been accepted. Notify interested listeners.
	 */
	private void notifyProposalAccepted(IContentProposal proposal) {
		if (DEBUG) {
			System.out.println("Notify listeners - proposal accepted."); //$NON-NLS-1$
		}
		final Object[] listenerArray = proposalListeners.getListeners();
		for (int i = 0; i < listenerArray.length; i++) {
			((IContentProposalListener) listenerArray[i])
					.proposalAccepted(proposal);
		}
	}

	/*
	 * The proposal popup has opened. Notify interested listeners.
	 */
	private void notifyPopupOpened() {
		if (DEBUG) {
			System.out.println("Notify listeners - popup opened."); //$NON-NLS-1$
		}
		final Object[] listenerArray = proposalListeners2.getListeners();
		for (int i = 0; i < listenerArray.length; i++) {
			((IContentProposalListener2) listenerArray[i])
					.proposalPopupOpened(this);
		}
	}

	/*
	 * The proposal popup has closed. Notify interested listeners.
	 */
	private void notifyPopupClosed() {
		if (DEBUG) {
			System.out.println("Notify listeners - popup closed."); //$NON-NLS-1$
		}
		final Object[] listenerArray = proposalListeners2.getListeners();
		for (int i = 0; i < listenerArray.length; i++) {
			((IContentProposalListener2) listenerArray[i])
					.proposalPopupClosed(this);
		}
	}

	/**
	 * Returns whether the content proposal popup has the focus. This includes
	 * both the primary popup and any secondary info popup that may have focus.
	 * 
	 * @return <code>true</code> if the proposal popup or its secondary info
	 *        popup has the focus
	 * @since 3.4
	 */
	public boolean hasProposalPopupFocus() {
		return popup != null && popup.hasFocus();
	}

	/*
	 * Return whether the control content is empty
	 */
	private boolean isControlContentEmpty() {
		return getControlContentAdapter().getControlContents(getControl())
				.length() == 0;
	}
	
	/*
	 * The popup has just opened, but listeners have not yet
	 * been notified.  Perform any cleanup that is needed.
	 */
	private void internalPopupOpened() {
		// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=243612
		if (control instanceof Combo) {
			((Combo)control).setListVisible(false);
		}
	}
	
	/*
	 * Return whether a proposal popup should remain open.
	 * If it was autoactivated by specific characters, and 
	 * none of those characters remain, then it should not remain
	 * open.  This method should not be used to determine
	 * whether autoactivation has occurred or should occur, only whether
	 * the circumstances would dictate that a popup remain open.
	 */
	private boolean shouldPopupRemainOpen() {
		// If we always autoactivate or never autoactivate, it should remain open
		if (autoActivateString == null || autoActivateString.length() == 0)
			return true;
		String content = getControlContentAdapter().getControlContents(getControl());
		for (int i=0; i<autoActivateString.length(); i++) {
			if (content.indexOf(autoActivateString.charAt(i)) >= 0)
				return true;
		}
		return false;
	}
	
	/*
	 * Return whether this adapter is configured for autoactivation, by
	 * specific characters or by any characters.
	 */
	private boolean allowsAutoActivate() {
		return (autoActivateString != null && autoActivateString.length() > 0) // there are specific autoactivation chars supplied
		  || (autoActivateString == null && triggerKeyStroke == null);    // we autoactivate on everything
	}
	
	/**
	 * Sets focus to the proposal popup. If the proposal popup is not opened,
	 * this method is ignored. If the secondary popup has focus, focus is
	 * returned to the main proposal popup.
	 * 
	 * @since 3.6
	 */
	public void setProposalPopupFocus() {
		if (isValid() && popup != null)
			popup.getShell().setFocus();
	}
	
	/**
	 * Answers a boolean indicating whether the main proposal popup is open.
	 * 
	 * @return <code>true</code> if the proposal popup is open, and
	 *        <code>false</code> if it is not.
	 * 
	 * @since 3.6
	 */
	public boolean isProposalPopupOpen() {
		if (isValid() && popup != null)
			return true;
		return false;
	}

}
